// This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International  
// https://creativecommons.org/licenses/by-nc-sa/4.0/
// © BigBeluga

//@version=6
indicator("Dynamic Liquidity Clouds [BigBeluga]", overlay = true, max_boxes_count = 500, max_lines_count = 500, max_labels_count = 500)

// ＩＮＰＵＴＳ ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――{
// Liquidity Engine Settings
pivotLength = input.int(5, title="Pivot Window", minval=2, group="Liquidity Engine", tooltip="Lookback/lookahead window for detecting swing highs and lows.")
cloudWidth  = input.int(100, title="Max Cloud Width (Bars)", minval=10, group="Liquidity Engine", tooltip="Maximum horizontal span (in bars) for the nested liquidity clouds.")
atrLen      = input.int(14, title="ATR Length", minval=1, group="Liquidity Engine", tooltip="ATR period used to calculate cloud vertical height.")
atrMult     = input.float(0.1, title="ATR Multiplier", minval=0.01, step=0.05, group="Liquidity Engine", tooltip="Multiplier applied to ATR to determine vertical spacing of cloud layers.")
cloudCount  = input.int(6, title="Cloud Layers Count", minval=1, maxval=10, group="Liquidity Engine", tooltip="Number of nested box layers rendered per liquidity pool.")

// Aesthetic Palette - Liquidity Clouds
bslColor    = input.color(#e57373, title="Buy-Side Liquidity (BSL) Cloud", group="Color Palette", tooltip="Base color for the nested cloud stack at pivot highs (above price).")
sslColor    = input.color(#26a69a, title="Sell-Side Liquidity (SSL) Cloud", group="Color Palette", tooltip="Base color for the nested cloud stack at pivot lows (below price).")
sweptColor  = input.color(#808080, title="Swept Cloud Color", group="Color Palette", tooltip="Color applied to liquidity clouds after price sweeps them.")
labelColor  = input.color(#ffffff, title="Label Text Color", group="Color Palette", tooltip="Text color for BSL/SSL target badges.")

// Sweep Detection
sweepDetection = input.bool(true, title="Enable Sweep Detection", group="Sweep Settings", tooltip="Mark liquidity pools as swept once price crosses the outer boundary.")
sweepSrc       = input.string("Close", title="Sweep Source", options=["Close", "Wick"], group="Sweep Settings", tooltip="Select whether candle Close or Wick (High/Low) triggers a liquidity sweep.")

// Visual Options
showLabels  = input.bool(true, title="Show BSL / SSL Labels", group="Visual Options", tooltip="Toggle BSL and SSL text badges on active liquidity pivots.")
labelSize   = input.string(size.small, title="Label Size", options=[size.tiny, size.small, size.normal, size.large, size.huge], group="Visual Options", tooltip="Select size for BSL and SSL target badges.")
// }

// ＴＹＰＥＳ ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――{
type liqNode
    int     pivotBar
    int     rightBar
    float   pivotPrice
    float   outerTop
    float   outerBot
    box[]   boxes
    label   badge
    bool    swept
// }

// ＣＡＬＣＵＬＡＴＩＯＮＳ ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――{
float pHigh = ta.pivothigh(high, pivotLength, pivotLength)
float pLow  = ta.pivotlow(low, pivotLength, pivotLength)

float atrVal = ta.atr(atrLen)

var liqNode[] bslNodes = array.new<liqNode>()
var liqNode[] sslNodes = array.new<liqNode>()

// Volume normalization window — matched to the pivot-span sum scale
maxVolLookback = 100
int   spanBars  = 2 * pivotLength + 1
float volHi     = ta.highest(volume, maxVolLookback) * spanBars
float volLo     = ta.lowest(volume, maxVolLookback)  * spanBars
float volRange  = volHi - volLo

volRatio(float v) =>
    volRange > 0 ? (v - volLo) / volRange : 0.5

volTransp(float v) =>
    math.round(90 - volRatio(v) * 70)

volTextTransp(float v) =>
    math.round(70 - volRatio(v) * 70)

float vol = math.sum(volume, spanBars)

// --- Helper function to clean node objects ---
deleteNodeObjects(liqNode node) =>
    if array.size(node.boxes) > 0
        for j = 0 to array.size(node.boxes) - 1
            box.delete(array.get(node.boxes, j))
    if not na(node.badge)
        label.delete(node.badge)

// Pivot High → Buy-Side Liquidity (BSL) Cloud
if not na(pHigh)
    int   pHBar    = bar_index[pivotLength]
    float pVol     = vol
    int   transp   = volTransp(pVol)
    int   txtTsp   = volTextTransp(pVol)
    float maxHW    = cloudWidth * volRatio(pVol)
    int   rightBar = pHBar + int(maxHW)
    float step     = atrVal * atrMult
    box[] boxes    = array.new_box()
    
    for k = 1 to cloudCount
        float halfW = maxHW * k / cloudCount
        float bot   = pHigh + step * k
        float top   = pHigh + step * (k + cloudCount)
        box b = box.new(left=pHBar - int(halfW), top=top, right=pHBar + int(halfW), bottom=bot, border_color=na, bgcolor=color.new(bslColor, transp + 20))
        array.push(boxes, b)
        
    label lbl = na
    if showLabels
        lbl := label.new(x=pHBar, y=pHigh, text="BSL", color=na, textcolor=color.new(labelColor, txtTsp), style=label.style_label_down, size=labelSize)
        
    float outerTop = pHigh + step * (2 * cloudCount)
    array.push(bslNodes, liqNode.new(pHBar, rightBar, pHigh, outerTop, pHigh, boxes, lbl, false))

// Pivot Low → Sell-Side Liquidity (SSL) Cloud
if not na(pLow)
    int   pLBar    = bar_index[pivotLength]
    float pVol     = vol
    int   transp   = volTransp(pVol)
    int   txtTsp   = volTextTransp(pVol)
    float maxHW    = cloudWidth * volRatio(pVol)
    int   rightBar = pLBar + int(maxHW)
    float step     = atrVal * atrMult
    box[] boxes    = array.new_box()
    
    for k = 1 to cloudCount
        float halfW = maxHW * k / cloudCount
        float top   = pLow - step * k
        float bot   = pLow - step * (k + cloudCount)
        box b = box.new(left=pLBar - int(halfW), top=top, right=pLBar + int(halfW), bottom=bot, border_color=na, bgcolor=color.new(sslColor, transp + 20))
        array.push(boxes, b)
        
    label lbl = na
    if showLabels
        lbl := label.new(x=pLBar, y=pLow, text="SSL", color=na, textcolor=color.new(labelColor, txtTsp), style=label.style_label_up, size=labelSize)
        
    float outerBot = pLow - step * (2 * cloudCount)
    array.push(sslNodes, liqNode.new(pLBar, rightBar, pLow, pLow, outerBot, boxes, lbl, false))

// --- Sweep Detection ---
float sweepPriceHi = sweepSrc == "Close" ? close : high
float sweepPriceLo = sweepSrc == "Close" ? close : low

if sweepDetection
    if array.size(bslNodes) > 0
        for i = array.size(bslNodes) - 1 to 0
            liqNode n = array.get(bslNodes, i)
            if not n.swept and bar_index <= n.rightBar and sweepPriceHi > n.outerTop
                n.swept := true
                if array.size(n.boxes) > 0
                    for j = 0 to array.size(n.boxes) - 1
                        box b = array.get(n.boxes, j)
                        box.set_bgcolor(b, color.new(sweptColor, 90))
                if not na(n.badge)
                    label.set_text(n.badge, "BSL ✓")
                    label.set_textcolor(n.badge, color.lime)

    if array.size(sslNodes) > 0
        for i = array.size(sslNodes) - 1 to 0
            liqNode n = array.get(sslNodes, i)
            if not n.swept and bar_index <= n.rightBar and sweepPriceLo < n.outerBot
                n.swept := true
                if array.size(n.boxes) > 0
                    for j = 0 to array.size(n.boxes) - 1
                        box b = array.get(n.boxes, j)
                        box.set_bgcolor(b, color.new(sweptColor, 90))
                if not na(n.badge)
                    label.set_text(n.badge, "SSL ✓")
                    label.set_textcolor(n.badge, color.lime)

// Cap total nodes (prune oldest)
maxNodes = 60
if array.size(bslNodes) > maxNodes
    liqNode old = array.shift(bslNodes)
    deleteNodeObjects(old)

if array.size(sslNodes) > maxNodes
    liqNode old = array.shift(sslNodes)
    deleteNodeObjects(old)
// }